<?php

declare(strict_types=1);

namespace app\command\model;

use think\facade\Config;
use think\facade\Db;
use think\helper\Str;

/**
 * Esmvc 模型类
 * 说明：数据库账号必须拥有information_schema数据库(SCHEMATA表、TABLES表、COLUMNS表)的访问权限。
 * @version 1.0.0-alpha.220316
 */
class EsMvcModel
{
    // 存根模板目录
    protected $stubDir = '';

    // 数据类型名称
    const TypeOfNumber =  ['INT', 'SMALLINT', 'TINYINT', 'MEDIUMINT', 'BIGINT', 'DECIMAL', 'DOUBLE', 'FLOAT',];
    const TypeInDateTime = ['DATE', 'TIME', 'YEAR', 'DATETIME', 'TIMESTAMP'];
    const TypeOfString = ['CHAR', 'VARCHAR', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT'];

    /**
     * 返回所有列
     * @param string $tableName
     * @param string $dbName
     * @return array COLUMN_NAME, COLUMN_TYPE, COLUMN_DEFAULT, IS_NULLABLE, CHARACTER_SET_NAME, COLLATION_NAME, `PRIVILEGES`, 
     *      EXTRA, COLUMN_COMMENT, ORDINAL_POSITION, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, COLUMN_KEY
     */
    public function getColumns($tableName, $dbName = '')
    {        
        $field = 'COLUMN_NAME, COLUMN_TYPE, DATA_TYPE, COLUMN_DEFAULT, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, CHARACTER_SET_NAME, COLLATION_NAME, COLUMN_KEY, EXTRA, COLUMN_COMMENT, `PRIVILEGES`';
        $where = [];
        if ($dbName) {
            $where[] = ['TABLE_SCHEMA', '=', $dbName];
        }
        if ($tableName) {
            $where[] = ['TABLE_NAME', '=', $tableName];
        }
        $list = Db::table('information_schema.columns')->where($where)->order('ORDINAL_POSITION')->column($field, 'COLUMN_NAME');
        return $list;
    }

    /**
     * 返回所有数据表
     * @param string $db_name
     * @param array $allowTables
     * @return array TABLE_NAME, TABLE_TYPE, ENGINE, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, AUTO_INCREMENT, CREATE_TIME, UPDATE_TIME, TABLE_COLLATION, TABLE_COMMENT, CHARACTER_MAXIMUM_LENGTH
     */
    public function getTables($dbName = '', $allowTables = [], $field = '')
    {
        if (!$field) {
            $field = 'TABLE_NAME, TABLE_TYPE, ENGINE, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH, AUTO_INCREMENT, CREATE_TIME, UPDATE_TIME, TABLE_COLLATION, TABLE_COMMENT';
        }
        $where = [];
        if ($dbName) {
            $where[] = ['TABLE_SCHEMA', '=', $dbName];
        }
        if ($allowTables) {
            $where[] = ['TABLE_NAME', 'in', $allowTables];
        }
        $where[] = ['TABLE_TYPE', '=', 'BASE TABLE'];
        $list = Db::table('information_schema.TABLES')->where($where)->field($field)->order('TABLE_NAME')->column($field, 'TABLE_NAME');
        return $list;
    }

    /**
     * 返回所有数据库
     * @param string $dbName 数据库名称
     * @return array SCHEMA_NAME,DEFAULT_CHARACTER_SET_NAME,DEFAULT_COLLATION_NAME
     */
    public function getDatabases($dbName = '')
    {
        $field = 'SCHEMA_NAME,DEFAULT_CHARACTER_SET_NAME,DEFAULT_COLLATION_NAME';
        $where = [];
        if ($dbName) {
            $where[] = ['SCHEMA_NAME', '=', $dbName];
        } else {
            $where[] = ['SCHEMA_NAME', 'not in', ['sys', 'mysql', 'information_schema', 'performance_schema']];
        }
        $list = Db::table('information_schema.schemata')->field($field)->where($where)->order('SCHEMA_NAME')->column($field, 'SCHEMA_NAME');
        return $list;
    }

    /**
     * 返回空表的默认字段
     */
    public function getColumnsOfEmptyTable()
    {
        return [
            'id' => [
                'COLUMN_NAME' => 'id',
                'COLUMN_TYPE' => 'int(10) unsigned',
                'DATA_TYPE' => 'int',
                'COLUMN_DEFAULT' => NULL,
                'IS_NULLABLE' => 'NO',
                'CHARACTER_MAXIMUM_LENGTH' => NULL,
                'NUMERIC_PRECISION' => 10,
                'NUMERIC_SCALE' => 0,
                'COLUMN_COMMENT' => '主键ID',
            ],
            'name' => [
                'COLUMN_NAME' => 'name',
                'COLUMN_TYPE' => 'varchar(150)',
                'DATA_TYPE' => 'varchar',
                'COLUMN_DEFAULT' => '',
                'IS_NULLABLE' => 'NO',
                'CHARACTER_MAXIMUM_LENGTH' => 150,
                'NUMERIC_PRECISION' => NULL,
                'NUMERIC_SCALE' => NULL,
                'COLUMN_COMMENT' => '审核说明',
            ],
            'status' => [
                'COLUMN_NAME' => 'status',
                'COLUMN_TYPE' => 'tinyint(3) unsigned',
                'DATA_TYPE' => 'tinyint',
                'COLUMN_DEFAULT' => '0',
                'IS_NULLABLE' => 'NO',
                'CHARACTER_MAXIMUM_LENGTH' => NULL,
                'NUMERIC_PRECISION' => 3,
                'NUMERIC_SCALE' => 0,
                'COLUMN_COMMENT' => '状态(0=待审核&1=审核通过&2=审核驳回)',
            ],
            'created_at' => [
                'COLUMN_NAME' => 'created_at',
                'COLUMN_TYPE' => 'int(10) unsigned',
                'DATA_TYPE' => 'int',
                'COLUMN_DEFAULT' => NULL,
                'IS_NULLABLE' => 'YES',
                'CHARACTER_MAXIMUM_LENGTH' => NULL,
                'NUMERIC_PRECISION' => 10,
                'NUMERIC_SCALE' => 0,
                'COLUMN_COMMENT' => '申请时间',
            ],
            'updated_at' => [
                'COLUMN_NAME' => 'updated_at',
                'COLUMN_TYPE' => 'int(10) unsigned',
                'DATA_TYPE' => 'int',
                'COLUMN_DEFAULT' => '0',
                'IS_NULLABLE' => 'YES',
                'CHARACTER_MAXIMUM_LENGTH' => NULL,
                'NUMERIC_PRECISION' => 10,
                'NUMERIC_SCALE' => 0,
                'COLUMN_COMMENT' => '审核时间',
            ],
        ];
    }

    /**
     * 获取 Enum 数据
     * 
     * 请求字符串格式：(1=正常&0=禁止)
     */
    public function getEnumData($comment)
    {
        $data = [];
        $matches = [];
        preg_match_all('/\(([^\)]+)\)/', $comment, $matches);
        // 请求字符串格式：(1=正常&0=禁止)
        $result = [];
        foreach ($matches[1] as $match) {
            if (false === strpos($match, '=')) {
                continue;
            }
            parse_str($match, $result);
            $data = array_merge($data, $result);
        }
        return $data;
    }

    /**
     * 获取纯文本注释内容
     * 
     * 去除[], (), （）等内容。
     */
    public function comment2text($text)
    {
        $chars = [' ', '[', '(', '（', '{', ':'];
        foreach ($chars as $char) {
            if (strpos($text, $char) > 0) {
                $text = substr($text, 0, strpos($text, $char));
            }
        }
        return $text;
        // 去除 [], (), （）,[cdl]...[/cdl], [[...]] 等内容
        $patterns = ['/\[[^\]]+\]/', '/\([^\)]+\)/', '/\（[^）]+\）/'];
        $text = preg_replace($patterns, '', $text);
        // （）内容有中文符号的话会失效，所以要额外检查一遍
        $pos = strpos($text, '（');
        if ($pos) {
            $text = substr($text, 0, $pos);
        }
        return trim($text);
    }

    /**
     * 解析 CDL 声明。
     * 
     * CDL: 备注信息声明语法 (Comment Declaration Language)
     * @return false|array [component, view, required, search, param, disabled]
     */
    public function getCDLData($comment, $strict = false)
    {
        // CDL-V3 JSON 格式 [cdl]{}[/cdl]
        // 示例1： [cdl]{"component":"select","param":["table":"","textfield":"",...],...}[/cdl]
        // 示例2：分类ID [cdl]{"component":"select?table=eb_goods_category&textfield=category_name&idfield=id","required":true,"disabled":true,"view":"curd"}[/cdl]
        $data = [
            // 前端组件名称 text, number, date, color, email, url, radio, checkbox, select, imguploader
            'component' => 'text',
            // 前端视图显示状态 c=在创建表单显示,r=在资源列表显示,u=在更新表单显示,d=在删除资源显示
            'view' => 'crud',
            // 是否表单必填项目
            'required' => false,
            // 搜索模式。支持运算符：like,eq,gt,gte,lt,let。
            'search' => '',
            // 其他参数。例如层级 pid、category_id 等字段需要声明 table, textfield, valuefield 等额外参数。
            'param' => [],
            // 是否可用
            'disabled' => false,
        ];
        $patterns = [
            '/\[cdl\](.+)\[\/cdl\]/', // [cdl]...[/cdl]
            '/\[\[(.+)\]\]/', // [[...]]
        ];
        $matches = [];
        $matchesTemp = [];
        foreach ($patterns as $pattern) {
            $matchesTemp = [];
            $affected = preg_match_all($pattern, $comment, $matchesTemp);
            if(!$affected){
                continue;
            }
            if (isset($matchesTemp[1][0])) {
                $matches[] = trim($matchesTemp[1][0]);
            }
        }
        if (!$matches) {
            return false;
        }
        $kvData = [];
        foreach ($matches as $text) {
            if ($text[0] == '{') {
                $kvData = json_decode($text, true);
                if (!$kvData) {
                    continue;
                }
            } else {
                parse_str($text, $kvData);
            }
            foreach ($kvData as $k => $v) {
                // 严格模式下，过滤无效名称
                if ($strict && !array_key_exists($k, $data)) {
                    continue;
                }
                // 解析额外参数
                $pos = strpos($v, '?');
                if ($k == 'component' && (false !== $pos)) {
                    $data['component'] = substr($v, 0, $pos);
                    parse_str(substr($v, $pos + 1), $data['param']);
                    continue;
                }
                $data[$k] = $v;
            }
        }
        return $data;
    }

    /**
     * 入口方法
     * @param string $app 应用名称（影响控制器子目录、视图子目录）。例如：admin, chadmin, mchadmin 等。
     * @param string $table 数据表名称，多个之间以逗号隔开。
     * @param string $object 目标对象，多个之间以逗号隔开。(m=模型文件,v=视图文件,c=控制器文件,r=路由文件)。
     * @param bool $isForce 如果目标文件已存在，是否覆盖。默认值 false。
     * @param bool $isRemove 是否删除目标文件。默认值 false。
     */
    public function run(string $app, string $table, string $object, bool $isForce = false, bool $isRemove = false)
    {
        $database = Config::get('database.default.database', env('database.database', ''));
        // 存根文件路径
        $this->stubDir = app_path('command/assets/esmvc');
        // 目标对象 --object=all/m,v,c (m=模型文件,v=视图文件,c=控制器文件,r=路由文件,p=菜单权限)
        $objectArr = $object == 'all' ? ['m', 'v', 'c', 'r', 'p'] : array_filter(explode(',', $object));
        // 数据表 --table=all/tbl1,tbl2...
        $tableArr = [];
        if ($table == 'all') {
            $tableArr = $this->getTables($database);
        } else {
            $emptyTables = [];
            $tableNames = array_filter(explode(',', $table), function ($v) use (&$emptyTables) {
                $v = trim($v);
                if (!$v) {
                    return false;
                }
                if (0 === strpos($v, '?')) {
                    $emptyTables[] = substr($v, 1);
                    return false;
                }
                return true;
            });
            if ($tableNames) {
                $tableArr = $this->getTables($database, $tableNames);
            }
            if ($emptyTables) {
                foreach ($emptyTables as $tableName) {
                    $tableArr[$tableName] = [
                        'TABLE_COMMENT' => $tableName,
                        'IS_EMPTY' => true,
                    ];
                }
            }
        }
        if (!$tableArr) {
            echo '数据表不存在！';
            return;
        }
        // 业务类型 crud=增删改查, log=日志(无增删改查功能), tree=层级功能，带增删改查功能（比如商品分类表等层级关系的表格）
        $bizType = '';
        // 数据表的名称字段（ID是唯一主键字段，Name是唯一标题字段，一般取第一个varchar类型的字段名，如果没有varchar则默认为id字段）
        $nameField = 'id';
        // 指定数据表的所有数据列
        $columns = [];
        // 数据列备注信息中的预设值。请求字符串格式：(1=正常&0=禁止)
        $enumData = [];
        // 模型类的所有属性获取器
        $attrs = [];
        // 参数配置
        $params = [];
        foreach ($tableArr as $tableName => $tableInfo) {
            $bizType = 'crud';
            // 如果数据表名称以 _log 结尾（一般为日志表），则识别为日志类型。
            if (str_ends_with($tableName, '_log')) {
                $bizType = 'log';
            }
            // 如果数据表名称以 stat_ 开头（一般为统计表），则识别为日志类型。
            if (str_starts_with($tableName, 'stat_')) {
                $bizType = 'log';
            }
            if (isset($tableInfo['IS_EMPTY'])) {
                $columns = $this->getColumnsOfEmptyTable();
            } else {
                $columns = $this->getColumns($tableName, $database);
            }
            $attrs = [];
            foreach ($columns as $column) {
                if (!$nameField && $column['DATA_TYPE'] == 'varchar') {
                    $nameField = $column['COLUMN_NAME'];
                }
                $enumData = $this->getEnumData($column['COLUMN_COMMENT']);
                if ($enumData) {
                    $attrs[$column['COLUMN_NAME']] = [
                        'comment' => $this->comment2text($column['COLUMN_COMMENT']),
                        'data' => $enumData,
                    ];
                }
            }
            // 如果数据表包含pid字段，则识别为树形层级类型。
            if (isset($columns['pid'])) {
                $bizType = 'tree';
            }
            $tableCDLInfo = $this->getCDLData($tableInfo['TABLE_COMMENT']);
            if (!empty($tableCDLInfo['biz_type'])) {
                $bizType = $tableCDLInfo['biz_type'];
            }
            // app, module, name, table, class, tag, comment, is_force, is_remove, biz_type, columns, attrs
            $prefix = 'default';
            $name = $table;
            if (strpos($table, '_')) {
                list($prefix, $name) = explode('_', $table, 2);
            }
            $params = [
                // 应用名称
                'app' => $app,
                // 模块名称（即数据表的前缀）
                'module' => $prefix,
                // 名称（去除前缀的数据表名称）
                'name' => $name . '2',
                // 数据表名称（包含前缀的数据表全称）
                'table' => $tableName . '2',
                // 类名（帕斯卡命名法：首字母大写的驼峰命名法，逻辑断点(单词)都用大写字母标记）
                'class' => Str::studly($name),
                // 标签名（短横线分隔命名法：逻辑断点（单词）用短横线隔开）
                'tag' => str_replace('_', '-', $name),
                // 表格备注
                'comment' => $this->comment2text($tableInfo['TABLE_COMMENT']),
                // 是否强制覆盖已存在的文件
                'is_force' => $isForce,
                // 是否删除模式
                'is_remove' => $isRemove,
                // 业务类型 crud=增删改查, log=日志(无增删改查功能), tree=层级功能，带增删改查功能（比如商品分类表等层级关系的表格）
                'biz_type' => $bizType,
                // 指定数据表的所有数据列
                'columns' => $columns,
                // 标题字段（一般取第一个varchar类型的字段名称）
                'name_field' => $nameField,
                // 模型类的所有属性获取器
                'attrs' => $attrs,
            ];
            if (str_ends_with($params['comment'], '表')) {
                $params['comment'] = mb_substr($params['comment'], 0, -1);
            }
            if (in_array('m', $objectArr)) {
                echo $this->makeModel($params), PHP_EOL;
            }
            if (in_array('v', $objectArr)) {
                echo $this->makeView($params), PHP_EOL;
            }
            if (in_array('c', $objectArr)) {
                echo $this->makeController($params), PHP_EOL;
                echo $this->makeValidate($params), PHP_EOL;
            }
            if (in_array('r', $objectArr)) {
                echo $this->makeRoute($params), PHP_EOL;
            }
            if (in_array('p', $objectArr)) {
                echo $this->makePermission($params), PHP_EOL;
            }
        }
    }

    // 生成控制器文件
    public function makeController($params)
    {
        $outputFile = app_path("controller/{$params['app']}/{$params['module']}/") . "{$params['class']}.php";
        if ($params['is_remove']) {
            unlink($outputFile);
            return '[成功] 控制器文件删除成功！路径：' . $outputFile;
        }
        if (is_file($outputFile) && !$params['is_force']) {
            return '[警告] 控制器文件已存在，如需覆盖，请使用 --force=y 参数！路径：' . $outputFile;
        }
        $stubFiles = [
            'crud' => 'Crud',
            'log' => 'Log',
            'tree' => 'Tree',
        ];
        $stubFile = $this->stubDir . 'controller/' . $stubFiles[$params['biz_type']] . '.php';


        $content = include $stubFile;
        // $content = file_get_contents($stubFile);
        // $content = str_replace(array_keys($replaceData), array_values($replaceData), $content);
        if (false === file_put_contents($outputFile, $content)) {
            return '[失败] 控制器文件写入失败！路径：' . $outputFile;
        }
        return '[成功] 控制器文件创建成功！路径：' . $outputFile;
    }

    // 生成校验文件
    public function makeValidate($params)
    {
        $outputFile = app_path("validate/{$params['app']}/{$params['module']}/") . "{$params['class']}Validate.php";
        if ($params['is_remove']) {
            unlink($outputFile);
            return '[成功] 控制器文件删除成功！路径：' . $outputFile;
        }
        if (is_file($outputFile) && !$params['is_force']) {
            return '[警告] 控制器文件已存在，如需覆盖，请使用 --force=y 参数！路径：' . $outputFile;
        }
        $stubFile = $this->stubDir . 'validate/Validate.php';

        $content = include $stubFile;
        // $content = file_get_contents($stubFile);
        // $content = str_replace(array_keys($replaceData), array_values($replaceData), $content);
        if (false === file_put_contents($outputFile, $content)) {
            return '[失败] 校验文件写入失败！路径：' . $outputFile;
        }
        return '[成功] 校验文件创建成功！路径：' . $outputFile;
    }

    // 生成数据模型文件
    public function makeModel($params)
    {
        $outputFile = app_path('model') . $params['class'] . 'Model.php';
        if ($params['is_remove']) {
            unlink($outputFile);
            return '[成功] 数据模型文件删除成功！路径：' . $outputFile;
        }
        if (is_file($outputFile) && !$params['is_force']) {
            return '[警告] 数据模型文件已存在，如需覆盖，请使用 --force=y 参数！路径：' . $outputFile;
        }
        $stubFiles = [
            'crud' => 'Crud',
            'log' => 'Log',
            'tree' => 'Tree',
        ];
        $stubFile = $this->stubDir . 'model/' . $stubFiles[$params['biz_type']] . 'Model.php';

        // 生成模型实例的默认值
        $attrData = [];
        $attrAppendData = [];
        $enumData = [];
        foreach ($params['attrs'] as $columnName => $attrInfo) {
            $columnComment = $attrInfo['comment'];
            $studlyName = Str::studly($columnName);
            $enumData = [];
            foreach ($attrInfo['data'] as $v => $text) {
                $v = var_export($v, true);
                $enumData[] = "['text' => '{$text}', 'value' => {$v}],";
            }
            $enumDataStr = implode(PHP_EOL, $enumData);
            // 访问器规则：
            $attrData[] = <<<getter
            /**
             * 返回 {$columnComment} 访问器的数据
             * @return array
             */
            public function get{$studlyName}DataAttr(): array
            {
                return [
                    {$enumDataStr}
                ];
            }
            /**
             * 返回 {$columnComment} 访问器的值
             * @return string
             */
            public function get{$studlyName}TextAttr(\$value, \$data)
            {
                \$value = \$data['{$columnName}'];
                if (is_numeric(\$value)) {
                    \$value = intval(\$value);
                }
                \$data = (array) \$this->get{$studlyName}DataAttr();
                \$result = array_filter(\$data, function (\$v) use (\$value) {
                    return \$v['value'] == \$value;
                }, ARRAY_FILTER_USE_BOTH);
                return \$result ? current(\$result)['text'] : \$value;
            }
            getter;
            // 规则：->append(['status_text','gender_text'])
            $attrAppendData[] = $columnName . '_text';
        }
        // 替换数据
        $replaceData = [
            // 数据表名称
            '__table__' => $params['table'],
            // 数据表备注
            '__comment__' => $params['comment'],
            // 类名（帕斯卡命名法：首字母大写的驼峰命名法）
            '__class__' => $params['class'],
            // 模型属性获取器的数据
            '__attr_getter__' => implode(PHP_EOL, $attrData),
            // 模型属性获取器的追加
            '__attr_append__' => $attrAppendData ? ('\'' . implode('\', \'', $attrAppendData) . '\'') : '',
        ];
        $content = include $stubFile;
        // $content = file_get_contents($stubFile);
        // $content = str_replace(array_keys($replaceData), array_values($replaceData), $content);
        if (false === file_put_contents($outputFile, $content)) {
            return '[失败] 数据模型文件写入失败！路径：' . $outputFile;
        }
        return '[成功] 模型文件创建成功！路径：' . $outputFile;
    }

    // 生成视图文件
    public function makeView($params)
    {
        $outputFiles = ['index.html', 'edit.html'];
        $outputDir = public_path($params['app'] . '/' . $params['tag']);
        if ($params['is_remove']) {
            foreach ($outputFiles as $file) {
                unlink($outputDir . $file);
            }
            return '[成功] 视图文件删除成功！路径：' . $outputDir;
        }
        foreach ($outputFiles as $file) {
            if (is_file($outputDir . $file) && !$params['is_force']) {
                return '[警告] 视图文件已存在，如需覆盖，请使用 --force=y 参数！路径：' . $outputDir . $file;
            }
        }
        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0777, true);
        }
        $stubDir = $this->stubDir . 'view/' . $params['biz_type'] . '/';

        // 生成数据列配置
        $columnsDataOfIndex = [];
        $columnsDataOfEdit = [];
        $ruleData = [];
        $numberTypes = ['INT', 'SMALLINT', 'TINYINT', 'MEDIUMINT', 'BIGINT', 'DECIMAL', 'DOUBLE', 'FLOAT',];
        $dateTypes = ['DATE' => 'date', 'TIME' => 'time', 'DATETIME' => 'datetime-local'];
        $inputComponents = ['email' => 'email', 'mobile' => 'mobile', 'avatar' => 'imguploader', 'icon' => 'imguploader'];
        // 过滤视图字段
        $skipNames = ['created_at', 'updated_at', 'deleted_at'];
        foreach ($params['columns'] as $columnName => $column) {
            if (in_array($columnName, $skipNames)) {
                continue;
            }
            $cdlInfo = $this->getCDLData($column['COLUMN_COMMENT']);
            if (false !== $cdlInfo && $cdlInfo['disabled']) {
                continue;
            }
            $dataType = strtoupper($column['DATA_TYPE']);
            // 规则：{ name: "id", text: "ID", component: "text", view: "r" },
            $ruleData['name'] = $columnName;
            $ruleData['text'] = $this->comment2text($column['COLUMN_COMMENT']);
            if ($columnName == 'id') {
                $ruleData['text'] = 'ID';
            } else {
                $ruleData['text'] = $this->comment2text($column['COLUMN_COMMENT']);
            }
            // 前端组件类型 text, number, date, color, email, url, radio, checkbox, select, imguploader
            if (false !== $cdlInfo) {
                $ruleData['component'] = $cdlInfo['component'];
            } else {
                $ruleData['component'] = 'text';
                if (in_array($dataType, $numberTypes)) {
                    $ruleData['component'] = 'number';
                }
                if (isset($dateTypes[$dataType])) {
                    $ruleData['component'] = $dateTypes[$dataType];
                }
            }
            if (isset($inputComponents[$columnName])) {
                $ruleData['component'] = $inputComponents[$columnName];
            }
            if (isset($params['attrs'][$columnName])) {
                $ruleData['component'] = 'select';
            }
            if ($column['IS_NULLABLE'] == 'NO') {
                $ruleData['required'] = true;
            }
            $ruleData['view'] = $columnName == 'id' ? 'r' : 'cru';
            if (false !== $cdlInfo) {
                if (isset($cdlInfo['view'])) {
                    $ruleData['view'] = $cdlInfo['view'];
                }
                if (strpos($cdlInfo['view'], 'r') && $dataType != 'TEXT') {
                    $columnsDataOfIndex[] = json_encode($ruleData, JSON_UNESCAPED_UNICODE);
                }
                if (strpos($cdlInfo['view'], 'c') || strpos($cdlInfo['view'], 'u')) {
                    $columnsDataOfEdit[] = json_encode($ruleData, JSON_UNESCAPED_UNICODE);
                }
                continue;
            }
            $columnsDataOfEdit[] = json_encode($ruleData, JSON_UNESCAPED_UNICODE);
            if ($dataType == 'TEXT') {
                continue;
            }
            if (isset($params['attrs'][$columnName])) {
                $ruleData['name'] =  "{$columnName}_text";
            }
            $columnsDataOfIndex[] = json_encode($ruleData, JSON_UNESCAPED_UNICODE);
        }

        // 替换数据
        $replaceData = [
            // 应用名称。例如：admin, chadmin, mchadmin
            '__app__' => $params['app'],
            // 数据表备注
            '__comment__' => $params['comment'],
            // 类名（帕斯卡命名法：首字母大写的驼峰命名法）
            '__class__' => $params['class'],
            // 标签名（短横线分隔命名法：逻辑断点（单词）用短横线隔开）
            '__tag__' => $params['tag'],
            // 数据列的配置内容
            '__columns__' => '',
        ];
        foreach ($outputFiles as $file) {
            switch ($file) {
                case 'edit.html':
                    $replaceData['__columns__'] = implode(',' . PHP_EOL, $columnsDataOfEdit);
                    break;
                default:
                    $replaceData['__columns__'] = implode(',' . PHP_EOL, $columnsDataOfIndex);
                    break;
            }
            if (!is_file($stubDir . $file)) {
                return '[错误] 视图存根文件不存在！路径：' . $stubDir . $file;
                continue;
            }
            $content = file_get_contents($stubDir . $file);
            $content = str_replace(array_keys($replaceData), array_values($replaceData), $content);
            if (false === file_put_contents($outputDir . $file, $content)) {
                return '[失败] 视图文件写入失败！路径：' . $outputDir . $file;
            }
        }
        return '[成功] 视图文件创建成功！路径：' . $outputDir;
    }

    // 生成路由文件
    public function makeRoute($params)
    {
        $outputFile = root_path('route') . '000Route.txt';
        $stubFile = $this->stubDir . 'route/route.php';

        // 替换数据
        $replaceData = [
            // 应用名称。例如：admin, chadmin, mchadmin
            '__app__' => $params['app'],
            // 类名（帕斯卡命名法：首字母大写的驼峰命名法）
            '__class__' => $params['class'],
            // 标签名（短横线分隔命名法：逻辑断点（单词）用短横线隔开）
            '__tag__' => $params['tag'],
        ];
        $content = include $stubFile;
        // $content = file_get_contents($stubFile);
        // $content = str_replace(array_keys($replaceData), array_values($replaceData), $content);
        if (false === file_put_contents($outputFile, $content, FILE_APPEND)) {
            return '[失败] 路由文件写入失败！路径：' . $outputFile;
        }
        return '[成功] 路由文件创建成功！路径：' . $outputFile;
    }

    // 生成权限菜单数据
    public function makePermission($params)
    {
        $outputFile = root_path('route') . '000Rule.txt';
        $stubFile = $this->stubDir . 'rule/rule.php';

        // 替换数据
        $replaceData = [
            // 数据表备注
            '__comment__' => $params['comment'],
            // 应用名称。例如：admin, chadmin, mchadmin
            '__app__' => $params['app'],
            // 类名（帕斯卡命名法：首字母大写的驼峰命名法）
            '__class__' => $params['class'],
            // 标签名（短横线分隔命名法：逻辑断点（单词）用短横线隔开）
            '__tag__' => $params['tag'],
        ];
        $content = file_get_contents($stubFile);
        $content = str_replace(array_keys($replaceData), array_values($replaceData), $content);
        if (false === file_put_contents($outputFile, $content, FILE_APPEND)) {
            return '[失败] 权限菜单文件写入失败！路径：' . $outputFile;
        }
        return '[成功] 权限菜单文件创建成功！路径：' . $outputFile;
    }
}
